By default, WebViews in iOS and macOS allow users to navigate to any URL by clicking links or through JavaScript-initiated navigations. This
behavior contrasts with native Android WebViews, which typically open new URLs in an external browser, thereby providing users with important context
like a visible URL bar. As Webviews in iOS and macOS deviate from this safer native default, arbitrary navigation is permitted unless explicitly
configured otherwise. This means the WebView behaves more like an embedded browser without the usual security indicators.
Permitting unrestricted navigation within an app’s WebView can lead to significant security vulnerabilities. For instance, an element within the
page (e.g., an ad) could redirect the user to a convincing phishing site designed to steal credentials. Since the WebView is part of the trusted
application and lacks standard browser interface elements, users are more susceptible to such deception. Furthermore, if a page with a Cross-Site
Scripting (XSS) vulnerability is loaded, the ability to navigate freely can expand the attack’s scope or be used to further mislead the user. This
loss of contextual security makes it difficult for users to identify and avoid malicious websites.
Ask Yourself Whether
- Your WebView loads content from arbitrary URLs or third-party sources (e.g., advertisements, external links)
- A malicious redirection within the WebView could lead to a phishing attack or credential theft
- The WebView displays sensitive information or interacts with features that could be compromised if the user is redirected to a malicious site
There is a risk if you answered yes to any of these questions.
Recommended Secure Coding Practices
Restrict Navigation
To prevent navigation to arbitrary URLs, all navigation attempts within the WebView need to be intercepted and validated.
For WKWebView, this is done by assigning an object that conforms to the WKNavigationDelegate protocol to the
navigationDelegate property. The webView(_:decidePolicyFor:decisionHandler:) callback in this delegate is called for every
navigation attempt (e.g., when a user clicks a link or JavaScript tries to change the location). This callback allows the application to inspect the
target URL and decide whether to allow the navigation by calling the decisionHandler with .allow or block it by calling it
with .cancel.
By implementing a navigation delegate, developers can enforce a strict navigation policy, such as blocking all navigation (most secure for static
content WebViews), allowing navigation only to a predefined set of trusted domains (allowlisting), or deciding to open certain links in an external
browser application rather than within the WebView itself.
Sensitive Code Example
By default, WKWebView allows arbitrary navigation. Setting no WKNavigationDelegate will allow the user to navigate to any
URL.
import WebKit
import UIKit
class ViewController: UIViewController, WKUIDelegate {
var webView: WKWebView!
override func loadView() {
let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.uiDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
let myURL = URL(string:"https://example.com/")
let myRequest = URLRequest(url: myURL!)
webView.load(myRequest) // Sensitive - all navigation is permitted by default
}
}
Compliant Solution
To restrict navigation, set a WKNavigationDelegate and implement the webView(_:decidePolicyFor:decisionHandler:) method
to validate the URL before allowing navigation.
import WebKit
import UIKit
class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {
var webView: WKWebView!
override func loadView() {
let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.uiDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
webView.navigationDelegate = self
let myURL = URL(string:"https://example.com/")
let myRequest = URLRequest(url: myURL!)
webView.load(myRequest)
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url, url.host == "example.com" {
decisionHandler(.allow)
return
}
// This could also be used to open the link in an external browser
decisionHandler(.cancel)
}
}
See